using System;
using System.Collections.Generic;
using System.Globalization;

namespace InfiniTec.Data
{
    public class PropertyCollection: Collection.Generic.KeyedCollection<IPropertyDefinition, Property>
    {
        private readonly List<IPropertyDefinition> _RemovedProperties = new List<IPropertyDefinition>();

    	public PropertyCollection() : base(property => ((IPropertyInfo) property).PropertyDefinition)
    	{
    	}

    	/// <summary>
        /// Provides access to a list of removed properties.
        /// </summary>
        /// <value>The removed properties.</value>
        public IList<IPropertyDefinition> RemovedProperties { get { return _RemovedProperties; } }

        ///<summary>
        ///Inserts an element into the <see cref="T:System.Collections.ObjectModel.KeyedCollection`2"></see> at the specified index.
        ///</summary>
        ///
        ///<param name="item">The object to insert.</param>
        ///<param name="index">The zero-based index at which item should be inserted.</param>
        ///<exception cref="T:System.ArgumentOutOfRangeException">index is less than 0.-or-index is greater than <see cref="P:System.Collections.ObjectModel.Collection`1.Count"></see>.</exception>
        protected override void InsertItem(int index, Property item)
        {
            if (item == null) throw new ArgumentNullException("item");
            
            IPropertyDefinition definition = ((IPropertyInfo) item).PropertyDefinition;

            base.InsertItem(index, item);

            if (_RemovedProperties.Contains(definition))
            {
                _RemovedProperties.Remove(definition);
            }
        }

        protected override void ClearItems()
        {
            foreach (Property property in this)
            {
                _RemovedProperties.Add(((IPropertyInfo) property).PropertyDefinition);
            }
            base.ClearItems();
        }

        protected override void RemoveItem(int index)
        {
            _RemovedProperties.Add(((IPropertyInfo)this[index]).PropertyDefinition);
            base.RemoveItem(index);
        }


        ///<summary>
        ///Replaces the item at the specified index with the specified item.
        ///</summary>
        ///
        ///<param name="item">The new item.</param>
        ///<param name="index">The zero-based index of the item to be replaced.</param>
        protected override void SetItem(int index, Property item)
        {
            _RemovedProperties.Add(((IPropertyInfo)this[index]).PropertyDefinition);
            base.SetItem(index, item);

            IPropertyDefinition info = ((IPropertyInfo) item).PropertyDefinition;
            if (_RemovedProperties.Contains(info))
            {
                _RemovedProperties.Remove(info);
            }
        }

        public Property<T> GetProperty<T>(PropertyDefinition<T> definition)
        {
            Property property = ResolveProperty(definition);

            return (Property<T>) property;
        }

        private Property ResolveProperty<T>(PropertyDefinition<T> definition)
        {
            Property result = this[definition];

            IPrototypeProperty prototype = result as IPrototypeProperty;

            if (prototype != null)
            {
                if (definition.IsMultivalued)
                {
                    result = prototype.ParseMultiValuedProperty<T>();
                }
                else
                {
                    result = prototype.ParseProperty<T>();
                }
                Dictionary[definition] = result;
            }

            //TODO: implement correct enum handling
            if (typeof(T).IsEnum && result is Property<int>)
            {
                result = new Property<T>(definition, (T)(object)((Property<int>)result).Value);
                Dictionary[definition] = result;
            }
            return result;
        }

        public MultiValuedProperty<T> GetMultiValuedProperty<T>(PropertyDefinition<T> definition)
        {
            Property property = ResolveProperty(definition);

            return (MultiValuedProperty<T>) property;
        }

        public void Add<T>(PropertyDefinition<T> definition, T value)
        {
            Add(new Property<T>(definition, value));
        }

        public void Add<T>(PropertyDefinition<T> definition, IEnumerable<T> values)
        {
            Add(new MultiValuedProperty<T>(definition, values));
        }


        public bool TryGetProperty<T>(PropertyDefinition<T> definition, out Property<T> result)
        {
            if (!Contains(definition))
            {
                result = null;
                return false;
            }
            
            result =  ResolveProperty(definition) as Property<T>;
            if (result == null) return false;

            if (result.Status != PropertyStatus.Ok)
            {
                result = null;
                return false;
            }
            return true;
        }

        public bool TryGetMultiValuedProperty<T>(PropertyDefinition<T> definition, out MultiValuedProperty<T> result)
        {
            if (!Contains(definition))
            {
                result = null;
                return false;
            }

            result = ResolveProperty(definition) as MultiValuedProperty<T>;
            if (result == null) return false;

            if (result.Status != PropertyStatus.Ok)
            {
                result = null;
                return false;
            }
            return true;
        }

        public T GetPropertyValue<T>(PropertyDefinition<T> definition)
        {
            return GetPropertyValue(definition, default(T));
        }

        public T GetPropertyValue<T>(PropertyDefinition<T> definition, T defaultValue)
        {
            Property<T> result;

            if (TryGetProperty(definition, out result))
            {
                return result.Value;
            }
            else
            {
                return defaultValue;
            }
        }

        public T? GetPropertyValue<T>(PropertyDefinition<T?> definition)
            where T:struct
        {
            return GetPropertyValue<T>(definition, null);
        }

        public T? GetPropertyValue<T>(PropertyDefinition<T?> definition, T? defaultValue)
            where T:struct
        {
            Property<T?> result;

            if (TryGetProperty(definition, out result))
            {
                return result.Value;
            }
            else
            {
                // No direct match found. This may be because the collection contains the property
                // as Property<T> but is queried for Property<T?>
                foreach (Property property in this)
                {
                    IPropertyInfo info = property;
                    if (info.PropertyDefinition.Name != definition.Name) continue;

                    return property.Status != PropertyStatus.Ok ? null : ConvertToValue<T>(info);
                }

                return defaultValue;
            }
        }

        private static T? ConvertToValue<T>(IPropertyInfo info)
            where T : struct
        {
            Type targetType = typeof (T);
            Type sourceType = info.PropertyDefinition.Type;

            if (info.PropertyValueIsNull) return null;                    

            if (targetType.IsAssignableFrom(info.PropertyDefinition.Type)) return (T) info.Value;

            if (targetType.IsEnum && sourceType == Enum<T>.UnderlyingType)
            {
                return Enum<T>.ConvertFrom(info.Value);
            }

            IConvertible convertible = info.Value as IConvertible;
            if (convertible != null) return (T?) convertible.ToType(targetType, CultureInfo.InvariantCulture);

            return null;
        }

        public IList<T> GetMultiValuedPropertyValue<T>(PropertyDefinition<T> definition)
        {
            MultiValuedProperty<T> result;

            if (TryGetMultiValuedProperty(definition, out result))
            {
                return result.Values;
            }
            else
            {
                return new T[0];
            }
        }

        public void SetPropertyValue<T>(PropertyDefinition<T> definition, T value)
        {
            Property<T> prop;
            Property result;

            if (ReferenceEquals(value, null))
            {
                Remove(definition);
                return;
            }

            if (TryGetProperty(definition, out prop))
            {
                prop.Value = value;
                result = prop;
            }
            else
            {
                result = CreateProperty(definition, value);
            }

            if (Contains(definition)) Remove(definition);
            Add(result);
        }

        private static Property CreateProperty<T>(PropertyDefinition<T> definition, T value)
        {
//            Type t = typeof(T);
//            Type propertyType;
//
//            propertyType = Nullable.GetUnderlyingType(t) ?? t;
//
//            t = typeof(Property<>);
//            t = t.MakeGenericType(propertyType);
         //   return (Property)Activator.CreateInstance(t, definition, Convert.ChangeType(value, propertyType));
            return new Property<T>(definition, value);
        }

        /// <summary>
        /// Ensures that the multivalued property with the specified name exists.
        /// </summary>
        /// <typeparam name="T">The type of the property</typeparam>
        /// <param name="definition">The property definition</param>
        public void EnsurePropertyExists<T>(PropertyDefinition<T> definition)
        {
            if (Contains(definition)) return;

            if (definition.IsMultivalued)
            {
                Add(new MultiValuedProperty<T>(definition));
            }
            else
            {
                Add(new Property<T>(definition));
            }
        }

        /// <summary>
        /// Copies the properties to the specified property collection.
        /// </summary>
        /// <param name="destination">The destination.</param>
        /// <param name="propertyNames">The property names.</param>
        /// <param name="markAsChanged"><see langword="true" />, if the properties should be marked as changed in the destination collection</param>
        public void CopyTo(PropertyCollection destination, bool markAsChanged, params IPropertyDefinition[] propertyNames)
        {
            Property property;

            if (destination == null)
            {
                throw new ArgumentNullException("destination");
            }

            if (propertyNames == null)
            {
                return;
            }

            foreach (IPropertyDefinition definition in propertyNames)
            {
                if (Contains(definition))
                {
                    if (destination.Contains(definition))
                    {
                        destination.Remove(definition);
                    }

                    if (!Contains(definition) || this[definition].Status != PropertyStatus.Ok)
                    {
                        continue;
                    }
                    property = this[definition].Clone();
                    if (markAsChanged)
                    {
                        property.SetChanged();
                    }
                    destination.Add(property);
                }

                if (definition.DependentProperties.Count > 0) 
                    CopyTo(destination, markAsChanged, new List<IPropertyDefinition>(definition.DependentProperties).ToArray());
            }
        }

        public void AddRange(IEnumerable<Property> properties)
        {
            if (properties == null) throw new ArgumentNullException("properties");

            foreach (Property property in properties)
            {
                if (Contains(property))
                {
                    Remove(property);
                }
                Add(property);
            }
        }
    }
}
